namespace Jsl.FxCop
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using Microsoft.FxCop.Sdk;

    /// <content>
    /// Holds the <see cref="FieldUsage"/> inner class.
    /// </content>
    public partial class MarkFieldReadOnly
    {
        /// <summary>
        /// Holds the most "important" instruction for accessing the field.
        /// </summary>
        /// <remarks>
        /// Note that I'm using the field name not the Field not as a key since it seems that Field node in generic
        /// types can be different depending on where you get them from.
        /// </remarks>
        private class FieldUsage : Dictionary<Field, Instruction>
        {
            private readonly TypeNode type;

            #region Constructors
            /// <summary>
            /// Initializes a new instance of the <see cref="FieldUsage"/> class.
            /// </summary>
            /// <param name="type">The type to get usage of.</param>
            public FieldUsage(TypeNode type)
            {
                if (type == null)
                {
                    throw new ArgumentNullException("type");
                }

                Contract.EndContractBlock();

                this.type = type;

                // Ignore any generated code. The Dictionary being empty implies no fields were found.
                if (this.type.IsGeneratedCode())
                {
                    return;
                }

                this.GetPrivateFields();

                // No need to process further if there are no fields.
                if (this.Count > 0)
                {
                    this.ProcessMethods(type);
                }
            }
            #endregion Constructors

            #region Public Properties
            /// <summary>
            /// Gets the type.
            /// </summary>
            /// <value>The type to get usage of.</value>
            public TypeNode Type
            {
                get
                {
                    return this.type;
                }
            }
            #endregion Public Properties

            #region Private Methods
            private static int GetOpCodeRank(OpCode operationCode)
            {
                switch (operationCode)
                {
                    case OpCode.Ldfld:
                    case OpCode.Ldsfld:
                        return 1;

                    case OpCode.Ldflda:
                    case OpCode.Ldsflda:
                        return 2;

                    case OpCode.Stfld:
                    case OpCode.Stsfld:
                        return 3;

                    default:
                        return 0;
                }
            }

            /// <summary>
            /// Build a list of all the private fields in the type.
            /// </summary>
            private void GetPrivateFields()
            {
                foreach (var member in this.Type.Members)
                {
                    var field = member as Field;
                    if (field != null)
                    {
                        if (field.IsPrivate && !field.IsInitOnly)
                        {
                            // It's common for the components field to be set to null and never used.
                            if (field.Name.Name != "components")
                            {
                                this.Add(field, null);
                            }
                        }
                    }
                }
            }

            private void ProcessMethod(Method method)
            {
                foreach (var instruction in method.Instructions)
                {
                    var field = instruction.Value as Field;
                    if (field != null)
                    {
                        // NOTE: For some reason on generic types the field.DeclaringType and the TypeNode we originally
                        // processed are different. The Field nodes are also different. The next two lines work around
                        // this issue.
                        if (field.DeclaringType.GetFullUnmangledNameWithTypeParameters() == this.Type.GetFullUnmangledNameWithTypeParameters())
                        {
                            field = this.Type.GetField(field.Name);
                            switch (instruction.OpCode)
                            {
                                case OpCode.Stfld:
                                case OpCode.Stsfld:
                                case OpCode.Ldflda:
                                case OpCode.Ldsflda:
                                    switch (method.NodeType)
                                    {
                                        case NodeType.InstanceInitializer:
                                            if (field.IsStatic)
                                            {
                                                this.Remove(field);
                                            }
                                            else
                                            {
                                                this.SetSourceContext(field, instruction);
                                            }

                                            break;

                                        case NodeType.StaticInitializer:
                                            if (field.IsStatic)
                                            {
                                                this.SetSourceContext(field, instruction);
                                            }
                                            else
                                            {
                                                this.Remove(field);
                                            }

                                            break;

                                        default:
                                            // The field is being set outside of a constructor so remove it from the list.
                                            this.Remove(field);
                                            break;
                                    }

                                    break;

                                case OpCode.Ldfld:
                                case OpCode.Ldsfld:
                                    // The field may never be set so just keep track of load fields also.
                                    this.SetSourceContext(field, instruction);
                                    break;
                            }
                        }
                    }
                }
            }

            private void ProcessMethods(TypeNode typeToProcess)
            {
                // Process all the methods (including constructors).
                foreach (var member in typeToProcess.Members)
                {
                    // Check all the methods for usage of this field.
                    var method = member as Method;
                    if (method != null)
                    {
                        this.ProcessMethod(method);
                        continue;
                    }

                    // Inner classes can access private fields so process them as well.
                    var innerType = member as TypeNode;
                    if (innerType != null)
                    {
                        this.ProcessMethods(innerType);
                    }
                }
            }

            /// <summary>
            /// Set the <see cref="SourceContext"/> if not already set.
            /// </summary>
            /// <param name="field">The field.</param>
            /// <param name="instruction">The instruction.</param>
            /// <remarks>
            /// We want to keep the first <see cref="SourceContext"/> so only set if not already set.
            /// </remarks>
            private void SetSourceContext(Field field, Instruction instruction)
            {
                Instruction currentInstruction;
                if (this.TryGetValue(field, out currentInstruction))
                {
                    // Replace the instruction if there isn't one already or this instruction is "better" than the current one.
                    if ((currentInstruction == null) || (GetOpCodeRank(instruction.OpCode) > GetOpCodeRank(currentInstruction.OpCode)))
                    {
                        this[field] = instruction;
                    }
                }
            }
            #endregion Private Methods
        }
    }
}